棒グラフ#
棒グラフ ( Bar Chart ) とは、主に質的変数を対象にして、棒の長さ[1]で数量を表す可視化手法です。 質的変数の量を見る最も一般的な方法の一つと言えます。

マンガ作者ごとの合計話数を表現した棒グラフを例に説明します。 棒グラフは、一つ目の 位置 スケール[2](上図「位置①」)で質的変数(上図「作者名」)の水準(上図「あだち充」さん)を指定し、それと直交する二つ目の 位置 スケール(上図「位置②」)を終点とする棒で量的変数(上図「合計話数」)の数量(「1269」話)を表します。
一つ目の位置スケールをX軸方向(横方向)に取り、二つ目の位置スケールをY軸方向(縦方向)に取ることが一般的ですが、逆にしても構いません。 前者は縦方向に棒が伸びることになるため、本書では 縦棒グラフ と呼びます。 一方で後者は横方向に棒が伸びるため、本書では 横棒グラフ と呼びます。
Plotlyでは、plotly.express.bar()で棒グラフを作成可能です。
# plotly.expressモジュールのインポート
# このモジュールは、インタラクティブな図の作成に特化
import plotly.express as px
# px.bar関数で棒グラフの作成
# x軸は'df'データフレームの'col_x'カラム、y軸は'col_y'カラムを使用
# 作成した図は'fig'変数に格納
fig = px.bar(df, x="col_x", y="col_y")
初期設定#
以降では、マンガ・アニメ・ゲームデータを可視化するための初期設定を行います。 なお、紙幅の都合のため、書籍版と一部構成が異なることにご注意ください。
Import#
必要なライブラリをImportします。
Show code cell content
# warningsモジュールのインポート
import warnings
# データ解析や機械学習のライブラリ使用時の警告を非表示にする目的で警告を無視
# 本書の文脈では、可視化の学習に議論を集中させるために選択した
# ただし、学習以外の場面で、警告を無視する設定は推奨しない
warnings.filterwarnings("ignore")
Show code cell content
# pathlibモジュールのインポート
# ファイルシステムのパスを扱う
from pathlib import Path
# typingモジュールからの型ヒント関連のインポート
# 関数やクラスの引数・返り値の型を注釈するためのツール
from typing import Any, Dict, List, Optional, Union
# numpy:数値計算ライブラリのインポート
# npという名前で参照可能
import numpy as np
# pandas:データ解析ライブラリのインポート
# pdという名前で参照可能
import pandas as pd
# plotly.expressのインポート
# インタラクティブなグラフ作成のライブラリ
# pxという名前で参照可能
import plotly.express as px
# plotly.graph_objectsからFigureクラスのインポート
# 型ヒントの利用を主目的とする
from plotly.graph_objects import Figure
なお、型ヒントについてはこちらを参照ください。
定数#
本Notebookで用いる定数を定義します。 なお、Pythonにおける定数の扱いについては、こちらを参照ください。
Show code cell content
# マンガデータ保存ディレクトリのパス
DIR_CM = Path("../../data/cm/input")
# アニメデータ保存ディレクトリのパス
DIR_AN = Path("../../data/an/input")
# ゲームデータ保存ディレクトリのパス
DIR_GM = Path("../../data/gm/input")
# マンガデータの分析結果の出力先ディレクトリのパス
DIR_OUT_CM = DIR_CM.parent / "output" / Path.cwd().parts[-1] / "bar"
# アニメデータの分析結果の出力先ディレクトリのパス
DIR_OUT_AN = DIR_AN.parent / "output" / Path.cwd().parts[-1] / "bar"
# ゲームデータの分析結果の出力先ディレクトリのパス
DIR_OUT_GM = DIR_GM.parent / "output" / Path.cwd().parts[-1] / "bar"
Show code cell content
# 読み込み対象ファイル名の定義
# Comic CollectionとCReaTor関連のファイル名
FN_CC_CRT = "cm_cc_crt.csv"
# Anime Episode関連のファイル名
FN_AE = "an_ae.csv"
# PacKaGeとPlatForm関連のファイル名
FN_PKG_PF = "gm_pkg_pf.csv"
Show code cell content
# 可視化に関する設定値の定義
# 可視化対象のマンガ作者数
N_CRT = 20
# 可視化対象のアニメ作品数
N_AC = 20
# 可視化対象のゲームプラットフォーム数
N_PF = 20
Show code cell content
# plotlyの描画設定の定義
# plotlyのグラフ描画用レンダラーの定義
# Jupyter Notebook環境のグラフ表示に適切なものを選択
RENDERER = "plotly_mimetype+notebook"
Show code cell content
# 質的変数の描画用のカラースケールの定義
# Okabe and Ito (2008)基準のカラーパレット
# 色の識別性が高く、多様な色覚の人々にも見やすい色組み合わせ
# 参考URL: https://jfly.uni-koeln.de/color/#pallet
OKABE_ITO = [
"#000000", # 黒 (Black)
"#E69F00", # 橙 (Orange)
"#56B4E9", # 薄青 (Sky Blue)
"#009E73", # 青緑 (Bluish Green)
"#F0E442", # 黄色 (Yellow)
"#0072B2", # 青 (Blue)
"#D55E00", # 赤紫 (Vermilion)
"#CC79A7", # 紫 (Reddish Purple)
]
関数#
以下では、本Notebookで用いる関数を定義します。
Show code cell content
def show_fig(fig: Figure) -> None:
"""
所定のレンダラーを用いてplotlyの図を表示
Jupyter Bookなどの環境での正確な表示を目的とする
Parameters
----------
fig : Figure
表示対象のplotly図
Returns
-------
None
"""
# 図の周囲の余白を設定
# t: 上余白
# l: 左余白
# r: 右余白
# b: 下余白
fig.update_layout(margin=dict(t=25, l=25, r=25, b=25))
# 所定のレンダラーで図を表示
fig.show(renderer=RENDERER)
Show code cell content
def save_df_to_csv(df: pd.DataFrame, dir_save: Path, fn_save: str) -> None:
"""
DataFrameをCSVファイルとして指定されたディレクトリに保存する関数
Parameters
----------
df : pd.DataFrame
保存対象となるDataFrame
dir_save : Path
出力先ディレクトリのパス
fn_save : str
保存するCSVファイルの名前(拡張子は含めない)
"""
# 出力先ディレクトリが存在しない場合は作成
dir_save.mkdir(parents=True, exist_ok=True)
# 出力先のパスを作成
p_save = dir_save / f"{fn_save}.csv"
# DataFrameをCSVファイルとして保存する
df.to_csv(p_save, index=False, encoding="utf-8-sig")
# 保存完了のメッセージを表示する
print(f"DataFrame is saved as '{p_save}'.")
Show code cell content
def format_cols(df: pd.DataFrame, cols_rename: Dict[str, str]) -> pd.DataFrame:
"""
指定されたカラムのみをデータフレームから抽出し、カラム名をリネームする関数
Parameters
----------
df : pd.DataFrame
入力データフレーム
cols_rename : Dict[str, str]
リネームしたいカラム名のマッピング(元のカラム名: 新しいカラム名)
Returns
-------
pd.DataFrame
カラムが抽出・リネームされたデータフレーム
"""
# 指定されたカラムのみを抽出し、リネーム
df = df[cols_rename.keys()].rename(columns=cols_rename)
return df
可視化例#
マンガデータ#
マンガ作者ごとの各話数を例に、可視化手法の使い方を説明します。
Show code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_cc_crt = pd.read_csv(DIR_CM / FN_CC_CRT)
Show code cell content
# 'crtname'を基に'n_ce'の合計を計算
df_cm = df_cc_crt.groupby("crtname")["n_ce"].sum().reset_index()
# 'n_ce'で降順ソートし、上位N_CRT件を選択
df_cm = df_cm.sort_values("n_ce", ascending=False, ignore_index=True).head(N_CRT)
# 描画に適したカラム名に変更
df_cm = df_cm.rename(columns={"crtname": "マンガ作者名", "n_ce": "合計話数"})
Show code cell content
# 可視化手法のDataFrameを確認
df_cm.head()
| マンガ作者名 | 合計話数 | |
|---|---|---|
| 0 | 水島新司 | 2798 |
| 1 | 秋本治 | 1979 |
| 2 | 梶原一騎 | 1873 |
| 3 | 高橋留美子 | 1736 |
| 4 | 浜岡賢次 | 1384 |
Show code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_cm, DIR_OUT_CM, "cm")
DataFrame is saved as '../../data/cm/output/05/bar/cm.csv'.
Show code cell source
# plotly.expressのbar関数で棒グラフ作成
# x軸: 'マンガ作者名', y軸: '合計話数'
fig = px.bar(df_cm, x="マンガ作者名", y="合計話数")
# 先に定義したshow_fig関数でグラフ表示
show_fig(fig)
上図は、マンガ作者名ごとの合計話数を表した棒グラフです。
本書で扱うデータ中で合計話数が最も多いのは、水島新司さんの2798話であることがわかります。
大きく差が開いていますが、次に秋本治さんや梶原一騎さんが続いています。
棒グラフに限らず、 名義尺度 の質的変数[3]を並べる場合はその順序に注意しましょう。 例えば、以下のようにアルファベット順にソートして表示することも考えられますが…、
Show code cell source
# '作者名'のアルファベット順でソートし棒グラフを作成
# x軸: 'マンガ作者名', y軸: '合計話数'
fig = px.bar(df_cm.sort_values("マンガ作者名"), x="マンガ作者名", y="合計話数")
# show_fig関数でグラフ表示
show_fig(fig)
これでは、離れた作者名同士の比較が難しくなります。例えば、蛭田達也さんと尾田栄一郎さんは、どちらのほうが合計話数が多いでしょうか?
合計話数順にソートされていないと、僅差での比較が困難です。
ただし、常に 数量順にソートすべきと判断するのは早計です。 例えば、マンガ作者名のアルファベット順と合計話数の関係を知りたい場合は、当然アルファベット順に並べるべきでしょう。 大切なのは、目的や用途に応じて、適切な表現方法を検討することです。
アニメデータ#
アニメ作品ごとの各話数を例に、可視化手法の使い方を説明します。
Show code cell content
# pandasのread_csv関数でCSVファイルをデータフレームとして読み込み
df_ae = pd.read_csv(DIR_AN / FN_AE)
Show code cell content
# 'acname' を基準にグループ化し、'aeid' のユニークな値の数を計算
# 各アニメ作品のエピソード数を取得
df_an = df_ae.groupby("acname")["aeid"].nunique().reset_index(name="n_ae")
# 'n_ae' の値で降順ソートし、上位 N_AC 件をデータ取得
df_an = df_an.sort_values("n_ae", ascending=False, ignore_index=True).head(N_AC)
# 可視化用にカラム名を変更
df_an = df_an.rename(columns={"acname": "アニメ作品名", "n_ae": "合計話数"})
Show code cell content
# 可視化対象のDataFrameを確認
df_an.head()
| アニメ作品名 | 合計話数 | |
|---|---|---|
| 0 | クレヨンしんちゃん | 1926 |
| 1 | 親子クラブ | 1363 |
| 2 | サザエさん | 1175 |
| 3 | ちびまる子ちゃん[新] | 994 |
| 4 | それいけ!アンパンマン | 958 |
Show code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_an, DIR_OUT_AN, "an")
DataFrame is saved as '../../data/an/output/05/bar/an.csv'.
Show code cell source
# plotly.expressのbar関数で棒グラフを作成
# x軸は'アニメ作品名'、y軸は'合計話数'
fig = px.bar(df_an, x="アニメ作品名", y="合計話数")
# show_fig関数で棒グラフを表示
show_fig(fig)
上図は、アニメ作品ごとの合計話数を表した棒グラフです。
このデータ中で最も合計話数が多いのは、クレヨンしんちゃんの1926話です。
紙兎 ロペ 笑う朝には福来たるって マジっすか!?等の長いアニメ作品名が存在するため下に間延びしてしまい、肝心の可視化領域が狭まってしまっています。アニメ作品名を斜めに書く[4]ことでスペースを圧縮する方法も考えられますが、作品名と棒の対応付けに苦労するでしょう。
プレゼンテーションであれ、ディスプレイであれ、可視化結果を描画する媒体は縦方向よりも横方向にスペースの余裕があることが多い[5]です。 縦棒グラフでスペースの問題に直面した際は、横棒グラフにすると問題を回避できます。
Show code cell source
# 横棒グラフの可視化
# plotly.expressのbar関数で横棒グラフ作成
# y軸は'アニメ作品名'、x軸は'合計話数'、'orientation="h"'で「横向き」を指定
# グラフの高さを500ピクセルに指定することで全件余裕を持って表示
fig = px.bar(df_an, y="アニメ作品名", x="合計話数", orientation="h", height=500)
# show_fig関数で図を表示
show_fig(fig)
この他にも、アニメ作品名に関して気になる点があったので、少し深掘りしてみましょう。
例えば、ドラえもん[新・第2期]という表記からもわかるように、一部のアニメ作品は放送期やシーズンで分割されている可能性が考えられます。
an_ae.csv中でacnameにドラえもんという表記が含まれるものを抽出してみましょう。
Show code cell content
# 'acname'毎の'aeid'の数と'date'の最初・最後の日付集計
df_tmp = (
df_ae.groupby("acname")
.agg(n_ae=("aeid", "nunique"), start_date=("date", "min"), end_date=("date", "max"))
.reset_index()
)
# 'acname'に「ドラえもん」を含むデータの抽出
df_tmp[df_tmp["acname"].str.contains("ドラえもん")]
| acname | n_ae | start_date | end_date | |
|---|---|---|---|---|
| 1914 | ドラえもん[新・第2期] | 608 | 2005-04-15 | 2016-12-31 |
| 1915 | ドラえもん[新] | 224 | 1999-12-03 | 2005-03-18 |
なお、str.containsは、pandasのSeriesオブジェクトに対して文字列の検索を行うためのメソッドです。
これを使用することで、Seriesの各要素に対して特定の文字列が含まれているかどうかを確認することができます。
このデータにおいてアニメ作品ドラえもんは二種類存在し、一つは1999-12-03から2005-03-18まで放送されたドラえもん[新]であり、もう一つは2005-04-15から2016-12-31に放送されたドラえもん[新・第2期]であることがわかります。
アニメ作品ドラえもんは一般に①1973年からのもの、②1979年からのもの、そして③2005年から現在までのものに分類されることが多いですが、このデータでは:
①:格納されていない
②:1999年12月から格納されている(
ドラえもん[新])③:2016年12月まで格納されている(
ドラえもん[新・第2期])
ことに注意が必要です。
同様にちびまる子ちゃん[新]という表記も気になったので見てみましょう。
Show code cell content
# 'acname'に「ちびまる子ちゃん」を含むデータの抽出
df_tmp[df_tmp["acname"].str.contains("ちびまる子ちゃん")]
| acname | n_ae | start_date | end_date | |
|---|---|---|---|---|
| 1029 | ちびまる子ちゃん[新] | 994 | 1999-12-05 | 2016-12-25 |
アニメ作品ちびまる子ちゃんは1990年1月7日から現在まで続く長寿作品ですが、このデータには1999-12-05から2016-12-25までしかないことには注意が必要です。
場合によっては、特定の期やシーズン(例:ドラえもん[新・第2期][6])に限定しないシリーズ(例:ドラえもん)全体としての傾向を分析したいことがあります。
an_ae.csvのasid(Anime Series ID、アニメシリーズID)を用いると便利です。
Show code cell content
# df_aeデータフレームを'date'列で昇順に並び替えて、'asid'でグループ化
# 各グループにおける'aeid'のユニークな値の数と、最初の'acname'を集計
# acnameを用いるのは、asname相当のデータが存在しないため
df_an2 = (
df_ae.sort_values("date")
.groupby("asid")
.agg({"aeid": "nunique", "acname": "first"})
).reset_index()
# 集計したデータフレームdf_an2を'aeid'(つまりシリーズ合計話数)で降順に並び替え、
# 上位N_AC件を取得して新たなdf_an2に更新
# ignore_index=Trueにより、インデックスを新しい連番にリセット
df_an2 = df_an2.sort_values("aeid", ascending=False, ignore_index=True).head(N_AC)
# 列名をより分かりやすい名称に変更
df_an2 = df_an2.rename(
columns={
"acname": "代表的なアニメ作品名",
"aeid": "シリーズ合計話数",
"asid": "アニメシリーズID",
}
)
Show code cell content
# 可視化対象とするDataFrameを確認
df_an2.head()
| アニメシリーズID | シリーズ合計話数 | 代表的なアニメ作品名 | |
|---|---|---|---|
| 0 | C1462 | 2084 | 忍たま 乱太郎[第1期] |
| 1 | C1327 | 1926 | クレヨンしんちゃん |
| 2 | C2158 | 1653 | おじゃる丸 |
| 3 | C1640 | 1363 | 親子クラブ |
| 4 | C4102 | 1245 | いない いない ばあっ![第4期] |
Show code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_an2, DIR_OUT_AN, "an2")
DataFrame is saved as '../../data/an/output/05/bar/an2.csv'.
Show code cell source
# 横棒グラフの可視化
# plotly.expressのbar関数で横棒グラフ作成
# y軸は'アニメ作品名'、x軸は'合計話数'、'orientation="h"'で「横向き」を指定
# グラフの高さを500ピクセルに指定することで全件余裕を持って表示
fig = px.bar(
df_an2, y="代表的なアニメ作品名", x="シリーズ合計話数", orientation="h", height=500
)
# show_fig関数で図を表示
show_fig(fig)
上図は、アニメシリーズごとの合計話数を表現した棒グラフです。
アニメ作品を基準に顔ぶれが変わっている点が興味深いです。
今回の可視化では、忍たま乱太郎シリーズが最も話数が多いことがわかりました。
ゲームデータ#
ゲームプラットフォームごとのゲームパッケージ数を例に、可視化手法を説明します。
Show code cell content
# pandasのread_csv関数でCSVファイルをデータフレームとして読み込み
df_pkg_pf = pd.read_csv(DIR_GM / FN_PKG_PF)
Show code cell content
# 'pfname'に基づき、'pkgid'のユニークな値の数の集計
df_gm = df_pkg_pf.groupby("pfname")["pkgid"].nunique().reset_index(name="n_pkg")
# 'n_pkg'の値で降順ソートし、上位N_PF件のデータを取得
df_gm = df_gm.sort_values("n_pkg", ascending=False).head(N_PF)
# カラム名を可視化に適したものに変更
df_gm = df_gm.rename(columns={"pfname": "プラットフォーム名", "n_pkg": "パッケージ数"})
Show code cell content
# 可視化対象とするDataFrameを確認
df_gm.head()
| プラットフォーム名 | パッケージ数 | |
|---|---|---|
| 40 | プレイステーション2 | 4216 |
| 39 | プレイステーション | 3750 |
| 44 | プレイステーション・ポータブル | 3417 |
| 41 | プレイステーション3 | 2501 |
| 43 | プレイステーションVita | 2348 |
Show code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_gm, DIR_OUT_GM, "gm")
DataFrame is saved as '../../data/gm/output/05/bar/gm.csv'.
Show code cell source
# plotly.expressのbar関数で棒グラフの作成
# x軸: プラットフォーム名、y軸: パッケージ数
fig = px.bar(df_gm, x="プラットフォーム名", y="パッケージ数")
# show_fig関数で図の表示
show_fig(fig)
上図は、ゲームプラットフォームごとのパッケージ数を表現した棒グラフです。
このデータにおいて最もゲームパッケージ数が多いプラットフォームはプレイステーション2であることがわかります。
その次にプレイステーション、プレイステーション・ポータブル、プレイステーション3、そしてプレイステーションVitaと続くのは印象的です。
メーカー[7]別に結果を表示したい場合はどうしたら良いでしょうか? 棒グラフでは 色 スケールを用いることで、第二の質的変数を表現することができます。
Show code cell content
# ゲームのプラットフォームとメーカーの対応辞書
# キー: プラットフォーム名、値: メーカー名の略称
pf2mk = {
"プレイステーション2": "ソニー",
"プレイステーション": "ソニー",
"プレイステーション・ポータブル": "ソニー",
"プレイステーション3": "ソニー",
"プレイステーションVita": "ソニー",
"ニンテンドー3DS": "任天堂",
"ニンテンドーDS": "任天堂",
"プレイステーション4": "ソニー",
"Xbox360": "マイクロソフト",
"Wii": "任天堂",
"スーパーファミコン": "任天堂",
"ゲームアーカイブス": "ソニー",
"ゲームボーイ": "任天堂",
"セガサターン": "セガ",
"WiiU": "任天堂",
"XboxOne": "マイクロソフト",
"ゲームボーイアドバンス": "任天堂",
"MicrosoftWindows": "マイクロソフト",
"ドリームキャスト": "セガ",
"メガドライブ": "セガ",
}
Show code cell content
# 可視化用に新たなDataFrameを作成
df_gm2 = df_gm.copy()
# 'プラットフォーム名'をキーとして辞書'pf2mk'からメーカー名を取得
# 新たな'メーカー名'カラムに格納
df_gm2["メーカー名"] = df_gm2["プラットフォーム名"].map(pf2mk)
Show code cell content
# 可視化対象とするDataFrameを確認
df_gm2.head()
| プラットフォーム名 | パッケージ数 | メーカー名 | |
|---|---|---|---|
| 40 | プレイステーション2 | 4216 | ソニー |
| 39 | プレイステーション | 3750 | ソニー |
| 44 | プレイステーション・ポータブル | 3417 | ソニー |
| 41 | プレイステーション3 | 2501 | ソニー |
| 43 | プレイステーションVita | 2348 | ソニー |
Show code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_gm2, DIR_OUT_GM, "gm2")
DataFrame is saved as '../../data/gm/output/05/bar/gm2.csv'.
Show code cell source
# 'プラットフォーム名'をx軸、'パッケージ数'をy軸として棒グラフ作成
# カラーは'メーカー名'に基づき、カラーパレットにはOKABE_ITOを使用
fig = px.bar(
df_gm2,
x="プラットフォーム名",
y="パッケージ数",
color="メーカー名",
height=400,
color_discrete_sequence=OKABE_ITO,
)
show_fig(fig)
上図は、プラットフォームごとのパッケージ数を表現した棒グラフです。 なお、プラットフォームのメーカーごとに棒の色を変えています。 これにより、メーカー同士の比較や、メーカー内での比較が容易になりました。
ただし、プレイステーション・ポータブル等のプラットフォーム名が長いため、肝心の棒の描画領域が狭くなってしまっています。
横棒グラフを試してみましょう。
Show code cell source
# 'パッケージ数'をx軸'プラットフォーム名'をy軸、として横棒グラフ作成
# カラーは'メーカー名'に基づき、カラーパレットにはOKABE_ITOを使用
fig = px.bar(
df_gm2,
x="パッケージ数",
y="プラットフォーム名",
color="メーカー名",
orientation="h",
height=400,
color_discrete_sequence=OKABE_ITO,
)
show_fig(fig)
棒の領域が広がりました。 さらに、凡例を図内に表示することで、描画領域を広げることができます。
Show code cell source
# 'update_layout'でグラフの凡例の位置を調整
# 凡例の右上の角を図の右上に固定 (x=0.99, y=0.99)
fig.update_layout(legend=dict(yanchor="top", y=0.99, xanchor="right", x=0.99))
show_fig(fig)
このように、目的に応じてレイアウトを柔軟に変更することが重要です。